内容相关开发:如何复用课程标签创建逻辑
课程标签的创建逻辑远比简单的 CRUD 复杂——用户可能传入已知 ID 的标签、传入新标签名称、甚至同时传入标签分类信息。本文将深入讲解 createTag 方法的完整实现,涵盖多场景数据处理、字典表查重、事务内批量创建等核心逻辑。
传入数据的多种场景
在为课程设置标签时,用户可能传递以下几种数据结构:
| 场景 | 传入数据 | 处理方式 |
|---|---|---|
| 已知 tagId | { courseId: 1, tagId: 3 } | 直接创建关联记录 |
| 已知多个 tagId | { courseId: 1, tags: [{ id: 1 }, { id: 2 }] } | 批量创建关联 |
| 传入标签名称 | { courseId: 1, tags: [{ name: "HTML" }] } | 查字典表 → 存在则关联,不存在则创建 |
| 传入名称+分类 | { courseId: 1, tags: [{ name: "HTML", type: { name: "前端" } }] } | 嵌套 connectOrCreate |
createTag 完整实现
方法签名
// course/course.service.ts
async createTag(
dto: CreateCourseTagDto,
prisma?: PrismaClient | Omit<PrismaClient, '$connect' | '$disconnect' | '$on' | '$transaction' | '$use' | '$extends'>,
) {
const tx = prisma || this.prisma;
const { tags, tagId, courseId } = dto;
// 场景一:已知 tagId,直接创建(最简情况)
if (tagId) {
return tx.courseTag.create({
data: { courseId, tagId },
});
}
// 场景二:传入 tags 数组,需要复杂处理
if (!tags || !Array.isArray(tags) || tags.length === 0) {
return null;
}
return tx.$transaction(async (prisma) => {
// 分离已知 ID 和未知 ID 的标签
const tagIds: number[] = tags
.filter((tag: any) => tag.id)
.map((tag: any) => tag.id);
const withoutIdTags = tags.filter(
(tag: any) => !tag.id,
);
// 已知 ID 的标签直接查询数据库中是否存在
if (withoutIdTags.length > 0) {
const tagNames = withoutIdTags.map((tag: any) => tag.name);
const existTags = await prisma.dictCourseTag.findMany({
where: { name: { in: tagNames } },
});
if (existTags && existTags.length > 0) {
// 将已存在的标签 ID 加入 tagIds
tagIds.push(...existTags.map((t) => t.id));
// 过滤出真正需要新建的标签
const needCreateTags = withoutIdTags.filter(
(tag) => !existTags.find((e) => e.name === tag.name),
);
// 处理需要新建的标签
if (needCreateTags.length > 0) {
const newTags = await Promise.all(
needCreateTags.map(async (tag: any) => {
const { type, ...restTagData } = tag;
let tagType = {};
if (type) {
tagType = {
courseType: {
connectOrCreate: {
where: { name: type.name },
create: { name: type.name },
},
},
};
}
const created = await prisma.dictCourseTag.create({
data: { ...restTagData, ...tagType },
});
return created;
}),
);
tagIds.push(...newTags.map((t) => t.id));
}
} else {
// 所有标签都需要新建
// ... 同上创建逻辑
}
}
// 批量创建已知 ID 的标签关联
const result: any = {};
if (tagIds.length > 0) {
await prisma.courseTag.createMany({
data: tagIds.map((id) => ({
courseId,
tagId: id,
})),
});
result.existCount = tagIds.length;
}
return result;
});
}
typescript
逻辑流程图
createTag(dto)
│
├── tagId 存在? ──是──→ 直接创建关联记录
│
└── tags 数组存在? ──否──→ 返回 null
│
▼
开启事务 $transaction
│
├── 分离 tags 为两类:
│ ├── 有 ID 的 → tagIds[]
│ └── 无 ID 的 → withoutIdTags[]
│
├── withoutIdTags 非空?
│ ├── 查询 dictCourseTag 是否存在同名标签
│ │ ├── 存在 → 将 ID 推入 tagIds[]
│ │ └── 不存在 → 需要创建新标签
│ │ └── 检查标签是否携带 type 分类
│ │ ├── 有 type → connectOrCreate 关联分类
│ │ └── 无 type → 直接创建标签
│ └── 新创建的标签 ID 推入 tagIds[]
│
└── tagIds 非空? → createMany 批量创建关联
text
关键技术点
1. createMany 批量创建
await prisma.courseTag.createMany({
data: tagIds.map((id) => ({
courseId,
tagId: id,
})),
});
typescript
createMany 一次插入多条记录,比循环调用 create 高效得多。
2. 字典表查重
const existTags = await prisma.dictCourseTag.findMany({
where: { name: { in: tagNames } },
});
typescript
使用 in 操作符批量查询,避免 N+1 查询问题。
3. connectOrCreate 嵌套
标签可能携带分类信息,需要在创建标签的同时关联或创建分类:
const created = await prisma.dictCourseTag.create({
data: {
name: 'HTML',
courseType: {
connectOrCreate: {
where: { name: '前端' },
create: { name: '前端' },
},
},
},
});
typescript
4. 可选 Prisma Client 参数
createTag 方法接受可选的 Prisma Client 参数,用于在事务中传入事务客户端:
async createTag(
dto: CreateCourseTagDto,
prisma?: PrismaClient, // 外部事务传入的客户端
) {
const tx = prisma || this.prisma; // 优先使用事务客户端
// ...
}
typescript
测试场景
场景一:已知 tagId 直接关联
POST /course/tags
{
"courseId": 4,
"tagId": 3
}
json
响应:直接创建关联记录。
场景二:混合传入(部分已知 ID + 部分新名称)
POST /course/tags
{
"courseId": 5,
"tags": [
{ "id": 3 },
{ "name": "CSS3", "type": { "name": "前端" } }
]
}
json
响应:
{
"exist": { "count": 1 },
"create": { "count": 1 }
}
json
场景三:全部为新标签
POST /course/tags
{
"courseId": 5,
"tags": [
{ "name": "HTML5", "type": { "name": "前端" } },
{ "name": "CSS4", "type": { "name": "前端" } }
]
}
json
响应:
{
"create": { "count": 2 }
}
json
小结
| 技术点 | 说明 |
|---|---|
| 数据分类 | 将传入标签按"已知 ID"和"未知 ID"分为两组分别处理 |
| 字典查重 | 使用 findMany + where: { name: { in: [...] } } 批量查重 |
| 事务保证 | 所有操作包裹在 $transaction 中确保数据一致性 |
| connectOrCreate | 标签分类的关联创建,存在则连接、不存在则新建 |
| createMany | 批量创建关联记录,性能优于循环 create |
| 可选 Prisma 参数 | 支持外部事务传入客户端实例,保证事务一致性 |
↑